package com.googlecode.strutson.utils

import static org.apache.commons.lang.StringUtils.uncapitalize as uncapitalize

import org.apache.commons.io.IOUtils
import org.objectweb.asm.*
import org.objectweb.asm.commons.EmptyVisitor
import org.objectweb.asm.signature.SignatureReader
import org.objectweb.asm.util.TraceSignatureVisitor


/**
 * Use ASM to get fields information from a class
 */
public abstract class ReflectionUtils {
    public static ClassInfo getClassInfo(File classFile) {
        InputStream is = new FileInputStream(classFile)
        try {
            ClassReader classReader = new ClassReader(is)
            CollectorVisitor visitor = new CollectorVisitor()
            classReader.accept(visitor, ClassReader.SKIP_DEBUG)
            return visitor.classInfo
        } finally {
            IOUtils.closeQuietly(is)
        }
    }
}

class ClassInfo {
    List<String> getters = new ArrayList()
    List<Map> setters = new ArrayList<Map>()
    Map<String, FieldConstraint> constraints = new HashMap<String, FieldConstraint>()
}

class FieldConstraint {
    String name
    String type
    Map constraints = new HashMap()
    //if this field is an enum, enumType will hold the type of the enum (duh)
    String enumType
}

enum AnnotationType {
    Column, Enumerated
}

class CollectorVisitor extends EmptyVisitor {
    //fields are visited before annotations
    String lastFieldVisited
    String lastFieldType
    AnnotationType lastAnnotationType
    ClassInfo classInfo = new ClassInfo()

    public AnnotationVisitor visitAnnotation(String name, boolean visible) {
        if ('Ljavax/persistence/Column;' == name && visible) {
            lastAnnotationType = AnnotationType.Column
            return super.visitAnnotation(name, visible)
        } else if ('Ljavax/persistence/Enumerated;' == name && visible) {
            FieldConstraint fieldConstraint = getFieldConstraint()
            fieldConstraint.type = 'enum'
            fieldConstraint.enumType = lastFieldType
            lastAnnotationType = AnnotationType.Enumerated
            return super.visitAnnotation(name, visible)
        }

        return null
    }

    /*
     *  this method is called vor each value of an annotation @XYZ(val1="val1", val2="val2"), it will be called
     *  for val1, and val2
     */

    public void visit(String name, Object value) {
        //add this annotation value to the constraints for the current fild
        FieldConstraint fieldConstraint = getFieldConstraint()
        fieldConstraint.constraints.put(name, value)
    }

    private FieldConstraint getFieldConstraint() {
        FieldConstraint fieldConstraint = classInfo.constraints.get(lastFieldVisited)
        if (!fieldConstraint) {
            fieldConstraint = new FieldConstraint();
            fieldConstraint.name = lastFieldVisited;
            fieldConstraint.type = getTypeName(lastFieldType)
            classInfo.constraints.put(lastFieldVisited, fieldConstraint)
        }
        return fieldConstraint
    }

    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        lastFieldVisited = name;
        lastFieldType = Type.getType(desc).className
        return super.visitField(access, name, desc, signature, value)
    }


    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if ((access & Opcodes.ACC_PUBLIC) && !(access & Opcodes.ACC_STATIC)) {
            if (name.startsWith('get') || name.startsWith('is')) {
                TraceSignatureVisitor visitor = new TraceSignatureVisitor(access)
                SignatureReader reader = new SignatureReader(desc);
                reader.accept(visitor);

                def typeName = getTypeName(visitor.returnType)
                classInfo.getters.add(name.startsWith('get') ? uncapitalize(name.substring(3)) : uncapitalize(name.substring(2)))
            }
            else if (name.startsWith('set') && name.size() > 3) {
                TraceSignatureVisitor visitor = new TraceSignatureVisitor(access)
                SignatureReader reader = new SignatureReader(desc);
                reader.accept(visitor);

                Type[] types = Type.getArgumentTypes(desc)
                if (types.length == 1) {
                    //visitor.declaration is in the form (CLASS_NAME,CLASS_NAME...)
                    def type = types[0]
                    def typeName = getTypeName(type.className)
                    classInfo.setters.add([
                            'name': uncapitalize(name.substring(3)),
                            'type': typeName
                    ])
                }
            }
        }
    }

    private String getTypeName(String type) {
        String typeName
        switch (type) {
            case Character.class.name:
            case 'char':
            case Byte.class.name:
            case 'byte':
            case Short.class.name:
            case 'short':
            case Integer.class.name:
            case 'int':
            case Long.class.name:
            case 'long':
            case Float.class.name:
            case 'float':
            case Double.class.name:
            case 'double':
                typeName = 'primitive'
                break
            case String.class.name:
                typeName = 'string'
                break
            case Boolean.TYPE.name:
                typeName = 'boolean'
                break;
            case Date.class.name:
            case Calendar.class.name:
                typeName = 'date'
                break
        }
        return typeName
    }

}

